package network.config;

import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Set;

import utils.Utils;

import network.AbstractNetworkInterface;

/**
 * NetworkConfiguration contains all information about 
 * network :
 *   - All the supported protocol
 *   - For each protocol, all the ports that must be used
 *   - The allowed network interfaces
 */
public class NetworkConfiguration {
	
	public enum Protocol {
		A1,
		A2,
		B,
		RT1,
		RT2
	}
	
	public enum PortType {
		UDP_RECEIVE,
		UDP_SEND,
		TCP_ACCEPT,
		TCP_CONNECT
	}
	
	public enum FeatureType {
		/* Allow user to send message. */
		CHAT,
		/* Allow user to create sub room of more than one user. */
		SUB_ROOM,
		/* Allow user to transfer file. */
		FILE_TRANSFER		
	}
	
	private static final EnumMap <Protocol, EnumMap <PortType, Integer>> DEFAULT_PORTS ;
	private static final Protocol DEFAULT_PROTOCOL = Protocol.A1 ;
	private static final EnumMap <Protocol, FeatureType[]> UNSUPPORTED_FEATURES ;
	
	static {
		int[][] ports = {
				{5500, 4321, itdb1.commun.NetworkSettings.UDP_PORT, 1024, 9001},
				{5500, 4321, itdb1.commun.NetworkSettings.UDP_PORT, 1024, 9001},
				{6500, 4321, itdb1.commun.NetworkSettings.TCP_PORT, 1024, 9001},
				{6500, 4321, itdb1.commun.NetworkSettings.TCP_PORT, 1024, 9001}
		} ;
		DEFAULT_PORTS = new EnumMap <Protocol, EnumMap <PortType, Integer>> (Protocol.class) ;
		int i = 0 ;
		for (Protocol p : Protocol.values()) {
			EnumMap <PortType, Integer> map = new EnumMap <PortType, Integer> (PortType.class) ;
			int j = 0 ;
			for (PortType pt : PortType.values()) {
				map.put(pt, ports[j++][i]) ;
			}
			i++ ;
			DEFAULT_PORTS.put(p, map) ;
		}
		UNSUPPORTED_FEATURES = new EnumMap <Protocol, FeatureType[]> (Protocol.class) ;
		UNSUPPORTED_FEATURES.put(Protocol.A1, new FeatureType[] {}) ;
		UNSUPPORTED_FEATURES.put(Protocol.A2, new FeatureType[] {FeatureType.SUB_ROOM}) ;
		UNSUPPORTED_FEATURES.put(Protocol.B, new FeatureType[] {FeatureType.FILE_TRANSFER, FeatureType.SUB_ROOM}) ;
		UNSUPPORTED_FEATURES.put(Protocol.RT1, new FeatureType[] {FeatureType.CHAT, FeatureType.FILE_TRANSFER, FeatureType.SUB_ROOM}) ;
		UNSUPPORTED_FEATURES.put(Protocol.RT2, new FeatureType[] {FeatureType.FILE_TRANSFER, FeatureType.SUB_ROOM}) ;
	}
	
	
	private EnumMap <Protocol, EnumMap <PortType, Integer>> currentPorts ;
	private Protocol currentProtocol = DEFAULT_PROTOCOL ;
	private ArrayList <NetworkConfigObserver> observers ;
	private Set <InterfaceAddress> allowedInterfaces ;

	public NetworkConfiguration () {
		this.currentPorts = new EnumMap <Protocol, EnumMap <PortType, Integer>> (Protocol.class) ;
		for (Protocol p : Protocol.values()) {
			this.currentPorts.put(p, new EnumMap <PortType, Integer> (DEFAULT_PORTS.get(p))) ;
		}
		this.observers = new ArrayList <NetworkConfigObserver> () ;
		this.allowedInterfaces = new HashSet <InterfaceAddress> (getDefaultAllowedInterfaces()) ;
	}
	
	/**
	 * Add an observer to the list of observers, fire all method of NetworkConfigObserver.
	 * 
	 * @param nco
	 */
	public void addObserver (NetworkConfigObserver nco) {
		this.observers.add(nco) ;
		fireProtocolToObserver() ;
		for (PortType p : PortType.values()) {
			firePortToObserver(p) ;
		}
		fireInterfacesToObserver() ;
	}
	
	/**
	 * Remove the specified observer.
	 * 
	 * @param nco
	 */
	public void removeObserver (NetworkConfigObserver nco) {
		this.observers.remove(nco) ;
	}
	
	private void fireInterfacesToObserver () {
		for (NetworkConfigObserver nco : this.observers) {
			nco.interfacesChanged(new HashSet <InterfaceAddress> (this.allowedInterfaces)) ;
		}
	}
	
	/**
	 * Notify all observer that protocol has changed.
	 */
	private void fireProtocolToObserver () {
		for (NetworkConfigObserver nco : this.observers) {
			nco.protocolChanged(this.currentProtocol, 
					new EnumMap <PortType, Integer> (this.currentPorts.get(this.currentProtocol)),
					new ArrayList <FeatureType> (Arrays.asList(UNSUPPORTED_FEATURES.get(this.currentProtocol)))) ;
		}
	}
	
	/**
	 * Notify all observers that specified port has changed for specified protocol
	 * 
	 * @param p
	 */
	private void firePortToObserver (Protocol protocol, PortType port) {
		for (NetworkConfigObserver nco : this.observers)  {
			nco.portChanged (protocol, port, this.currentPorts.get(protocol).get(port)) ;
		}
	}
	
	/**
	 * Notify all observers that specified port has changed for the current protocol
	 * 
	 * @param p
	 */
	private void firePortToObserver (PortType port) {
		firePortToObserver(this.currentProtocol, port) ;
	}
	
	/**
	 * @return true if message from the current address should be accepted
	 */
	public boolean isAllowedAddress (InetAddress addr) {
		boolean isLocal = false ;
		boolean isAllowed = false ;
		for (InterfaceAddress ia : AbstractNetworkInterface.getAllLocalInterfaces()) {
			if (ia.getAddress().equals(addr)) {
				isLocal = true ;
			}
		}
		for (InterfaceAddress ia : this.allowedInterfaces) {
			int prefix = ia.getNetworkPrefixLength() ;
			if (Utils.getNetworkAddress(addr, prefix).equals(Utils.getNetworkAddress(ia.getAddress(), prefix))) {
				isAllowed = true ;
			}
		}
		return (!isLocal) && isAllowed ;
	}
	
	/**
	 * @return The current allowed interfaces
	 */
	public Set <InterfaceAddress> getAllowedInterfaces () {
		return this.allowedInterfaces ;
	}
	
	/**
	 * @return The default allowed interfaces
	 */
	public Set <InterfaceAddress> getDefaultAllowedInterfaces () {
		Set <InterfaceAddress> defaultAllowed = new HashSet <InterfaceAddress> () ;
		for (InterfaceAddress ia : AbstractNetworkInterface.getAllLocalInterfaces()) {
			if (!ia.getAddress().isLoopbackAddress() && !ia.getAddress().isLinkLocalAddress()) {
				defaultAllowed.add(ia) ;
			}
		}	
		return defaultAllowed ;
	}
	
	/**
	 * Remove the specified interface from interface group
	 */
	public void removeAllowedInterface (InterfaceAddress interfaceAddress) {
		this.allowedInterfaces.remove(interfaceAddress) ;
		fireInterfacesToObserver() ;
	}
	
	/**
	 * Add the specified interface from interface group
	 */
	public void addAllowedInterface (InterfaceAddress interfaceAddress) {
		this.allowedInterfaces.add(interfaceAddress) ;
		fireInterfacesToObserver() ;
	}
	
	/**
	 * @return The default protocol
	 */
	public Protocol getDefaultProtocol () {
		return DEFAULT_PROTOCOL ;
	}
	
	/**
	 * @return The current protocol
	 */
	public Protocol getCurrentProtocol () {
		return this.currentProtocol ;
	}
	
	/**
	 * Set the current protocol
	 * 
	 * @param p
	 */
	public void setCurrentProtocol (Protocol p) {
		this.currentProtocol = p ;
		fireProtocolToObserver() ;
	}
	
	/**
	 * @return The default port for the specified protocol / port type
	 */
	public int getDefaultPort (Protocol protocol, PortType port) {
		return DEFAULT_PORTS.get(protocol).get(port) ;
	}
	
	/**
	 * @return The default port for the specified PortTYpe for current protocol
	 */
	public int getDefaultPort (PortType port) {
		return getDefaultPort (this.currentProtocol, port) ;
	}
	
	/**
	 * @return The port for the specified protocol / PortType
	 */
	public int getPort (Protocol protocol, PortType port) {
		return this.currentPorts.get(protocol).get(port) ;
	}

	/**
	 * @return The port for the specified PortType for current protocol
	 */
	public int getPort (PortType p) {
		return getPort(this.currentProtocol, p) ;
	}
	
	/**
	 * @param port
	 * @return true if the specified port is correct
	 */
	private static boolean isCorrectPort (int port) {
		return (port >= 1024) && (port < 65536) ;
	}
	
	/**
	 * Set the specified PortType for the specified Protocol to newPort
	 * 
	 * @throws BadPortException
	 */
	public void setPort (Protocol protocol, PortType port, int newPort) throws BadPortException {
		if (!isCorrectPort(newPort)) { 
			throw new BadPortException(port) ;
		}
		this.currentPorts.get(protocol).put(port, newPort) ;
	}

}
